今日目標,開始遊戲。
對,今天只有「開始」還不包含遊戲的過程,但在開始之前,我們要先定義一些之後遊戲過程方便操作的類別或實例。
先建立一個 package,名稱為 game。
我們首先定義一個玩家(player),以及它所包含的資訊(名稱、手牌等)、方法(出牌)。
package com.example.game;
import com.example.card.Card;
import lombok.Getter;
import lombok.Setter;
import java.util.ArrayList;
@Getter @Setter
public class Player {
private String name;
private ArrayList<Card> hands;
public Player(String name) {
this.name = name;
}
public int countHands() {
return this.hands.size();
}
public void play(ArrayList<Card> cards) {
for (Card card : cards) {
this.hands.remove(card);
}
}
}
play()
內的 this.hands.remove(card)
這行,remove 的實作是根據 Object.equals()
來比較的,所以我們需要回去修改 Card,加入自定義的 equals 方法,加入的片段程式碼為:
@Override
public boolean equals(Object object) {
// 如果 object 屬於 Card 實例
if (object instanceof Card) {
Card other = (Card) object;
// 比較 Suit 和 Number
return this.suit.equals(other.suit) && this.number.equals(other.number);
}
return false;
}
我們還需要一個類型是專門處理玩家打出的牌,透過維護一個 ArrayList<Card>
實現,並給予適當的方法。
package com.example.card;
import java.util.ArrayList;
import java.util.Comparator;
public class PlayedCards {
private final ArrayList<Card> cards;
public PlayedCards(ArrayList<Card> playedCards) {
this.cards = playedCards;
}
public ArrayList<Card> get() {
return this.cards;
}
}
跟前面的 UserStatus 類似,用來記錄各房間的遊戲狀態。
Status 包含玩家(players)、上一出牌的玩家(previousPlayer)、上一個玩家打出的牌(previousPlayedCards)、當前輪到的玩家(currentPlayer)。
GameStatus 則是藉由維護一個 HashMap,其中 key 為房號(roomId)、value 為狀態(status)。
package com.example.game;
import com.example.card.Card;
import com.example.card.PlayedCards;
import com.example.room.UserStatus;
import lombok.Getter;
import lombok.Setter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
@Getter @Setter
class Status {
private Player[] players = new Player[4];
private String previousPlayer = null;
private PlayedCards previousPlayedCards = null;
private String currentPlayer = null;
}
@Component
public class GameStatus {
@Autowired
private UserStatus userStatus;
private final Map<String, Status> game = new HashMap<>();
public void add(String roomId) {
this.game.put(roomId, new Status());
}
public void remove(String roomId) {
this.game.remove(roomId);
}
public void setPlayers(String roomId, Player[] players) {
this.game.get(roomId).setPlayers(players);
}
public Player[] getPlayers(String roomId) {
return this.game.get(roomId).getPlayers();
}
public void setPreviousPlayer(String roomId, String playerName) {
this.game.get(roomId).setPreviousPlayer(playerName);
}
public String getPreviousPlayer(String roomId) {
return this.game.get(roomId).getPreviousPlayer();
}
public void setPreviousPlayedCards(String roomId, PlayedCards playedCards) {
this.game.get(roomId).setPreviousPlayedCards(playedCards);
}
public PlayedCards getPreviousPlayedCards(String roomId) {
return this.game.get(roomId).getPreviousPlayedCards();
}
public void setCurrentPlayer(String roomId, String playerName) {
this.game.get(roomId).setCurrentPlayer(playerName);
}
public String getCurrentPlayer(String roomId) {
return this.game.get(roomId).getCurrentPlayer();
}
public ArrayList<Card> getHandsByPlayerName(String name) {
String roomId = this.userStatus.getUserRoomId(name);
Player[] players = this.getPlayers(roomId);
for (Player player : players) {
if (player.getName().equals(name)) {
return player.getHands();
}
}
return null;
}
public ArrayList<ArrayList<Card>> getAllPlayersHands(String roomId) {
ArrayList<ArrayList<Card>> result = new ArrayList<>();
Player[] players = this.game.get(roomId).getPlayers();
for (Player player : players) {
result.add(player.getHands());
}
return result;
}
}
再來,等房主開始遊戲時,我們需要對該房間的遊戲狀態做初始化,初始化的方法,我們透過 GameService 提供這項服務。
package com.example.game;
import com.example.card.Card;
import com.example.card.Deck;
import com.example.room.Room;
import com.example.room.RoomList;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.*;
@Service
public class GameService {
@Autowired
private RoomList roomList;
@Autowired
private GameStatus gameStatus;
public void initializeGameStatus(String roomId) {
Room room = roomList.getRoomById(roomId);
// 洗牌和發牌
Deck deck = new Deck();
deck.shuffle();
ArrayList<ArrayList<Card>> hands = deck.deal();
// 建立 Player,並分配手牌
ArrayList<String> roomMembers = room.getAllMembers();
LinkedHashMap<String, ArrayList<Card>> playerHands = new LinkedHashMap<>();
Player[] players = new Player[4];
for (int i=0; i<4; i++) {
Player newPlayer = new Player(roomMembers.get(i));
newPlayer.setHands(hands.get(i));
players[i] = newPlayer;
}
gameStatus.add(roomId);
gameStatus.setPlayers(roomId, players);
}
}
在遊戲開始時,我們要做遊戲狀態的初始化,讀者還記得什麼時候開始嗎?就是在所有人都準備好了(包含房主)就是開始遊戲!
readyToPlay()
,僅加入此片段程式碼到該函數最後:
if (response.isAllReady()) {
this.gameService.initializeGameStatus(roomId);
}
readyToPlay()
:
@MessageMapping("/ready")
public void readyToPlay(UserReadyMessage readyMessage, Principal principal) {
String username = principal.getName();
this.userStatus.setUserReady(username, readyMessage.isReady());
String roomId = this.userStatus.getUserRoomId(username);
Room room = roomList.getRoomById(roomId);
ArrayList<String> roomMembers = room.getAllMembers();
UserReadyResponse response = new UserReadyResponse();
int counter = 0;
for (String member : roomMembers) {
boolean isReady = this.userStatus.isUserReady(member);
if (isReady) {
counter += 1;
}
response.add(member, isReady);
}
String owner = room.getOwner();
if (counter == 4) {
response.setAllReady(true);
}
else {
this.userStatus.setUserReady(owner, false);
response.add(owner, false);
}
for (String member : roomMembers) {
response.setMessage("");
if (counter != 4 && member.equals(username)) {
if (member.equals(owner)) {
if (room.count() == 4) {
response.setMessage("尚有人未準備好開始遊戲!");
}
else {
response.setMessage("人數不足!");
}
}
else {
if (!readyMessage.isReady()) {
response.setMessage("請快點準備!");
}
}
}
simpMessagingTemplate.convertAndSendToUser(member, "/queue/ready", response);
}
// 這邊!!!
if (response.isAllReady()) {
this.gameService.initializeGameStatus(roomId);
}
}
room.js
的片段程式碼,在接收到大家都準備好時,跳轉到遊戲頁面,修改 subscribeReady()
,該部分的完整程式碼為:
function subscribeReady() {
websocket.subscribe("/user/queue/ready", (response) => {
response = JSON.parse(response.body);
let userStatus = response.userStatus;
for (let user in userStatus) {
let readyText = userStatus[user] ? '準備' : '';
if (user === owner) {
readyText = "房主"
}
$(`#user-${user} .user-ready`).text(readyText);
}
// 當全部都準備好,跳轉到遊戲畫面
if (response.allReady) {
window.location.href = `/game/${roomId}`;
}
})
}
最後,我們先簡單布置遊戲畫面,這邊先只放骨架,之後會透過 JavaScript 渲染。
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{layout.html}"
>
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div layout:fragment="content" class="card game-window">
<input id="my-username" type="hidden" th:value="${username}">
<div class="timer text-center">15</div>
<div class="other-hands-90 other-hands-left" id="user-3">
<div class="m-card text-center"></div>
<div class="m-card text-center"></div>
<div class="m-card text-center"></div>
<div class="m-card text-center"></div>
<div class="m-card text-center"></div>
<div class="m-card text-center"></div>
<div class="m-card text-center"></div>
<div class="m-card text-center"></div>
<div class="m-card text-center"></div>
<div class="m-card text-center"></div>
<div class="m-card text-center"></div>
<div class="m-card text-center"></div>
<div class="m-card text-center"></div>
</div>
<div class="other-hands" id="user-4">
<div class="m-card text-center"></div>
<div class="m-card text-center"></div>
<div class="m-card text-center"></div>
<div class="m-card text-center"></div>
<div class="m-card text-center"></div>
<div class="m-card text-center"></div>
<div class="m-card text-center"></div>
<div class="m-card text-center"></div>
<div class="m-card text-center"></div>
<div class="m-card text-center"></div>
<div class="m-card text-center"></div>
<div class="m-card text-center"></div>
<div class="m-card text-center"></div>
</div>
<div class="my-hands" id="user-1">
<div class="m-card text-center"></div>
<div class="m-card text-center"></div>
<div class="m-card text-center"></div>
<div class="m-card text-center"></div>
<div class="m-card text-center"></div>
<div class="m-card text-center"></div>
<div class="m-card text-center"></div>
<div class="m-card text-center"></div>
<div class="m-card text-center"></div>
<div class="m-card text-center"></div>
<div class="m-card text-center"></div>
<div class="m-card text-center"></div>
<div class="m-card text-center"></div>
</div>
<div class="other-hands-90 other-hands-right" id="user-2">
<div class="m-card text-center"></div>
<div class="m-card text-center"></div>
<div class="m-card text-center"></div>
<div class="m-card text-center"></div>
<div class="m-card text-center"></div>
<div class="m-card text-center"></div>
<div class="m-card text-center"></div>
<div class="m-card text-center"></div>
<div class="m-card text-center"></div>
<div class="m-card text-center"></div>
<div class="m-card text-center"></div>
<div class="m-card text-center"></div>
<div class="m-card text-center"></div>
</div>
<div class="card-type">
<div class="m-card text-center">
<div>
♠️
<br>
10
</div>
</div>
<div class="m-card text-center">
<div>
♠️
<br>
J
</div>
</div>
<div class="m-card text-center">
<div>
♠️
<br>
Q
</div>
</div>
<div class="m-card text-center">
<div>
♠️
<br>
K
</div>
</div>
<div class="m-card text-center">
<div>
♠️
<br>
A
</div>
</div>
</div>
<div class="action">
<button type="button" class="btn btn-outline-success btn-lg" id="button-play">出牌</button>
<button type="button" class="btn btn-outline-danger btn-lg" id="button-pass">PASS</button>
</div>
</div>
<div layout:fragment="js-and-css">
<!-- websockets -->
<script src="https://cdn.jsdelivr.net/npm/sockjs-client@1/dist/sockjs.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/stomp.js/2.3.3/stomp.js"
integrity="sha512-tL4PIUsPy+Rks1go4kQG8M8/ItpRMvKnbBjQm4d2DQnFwgcBYRRN00QdyQnWSCwNMsoY/MfJY8nHp2CzlNdtZA=="
crossorigin="anonymous"
referrerpolicy="no-referrer"></script>
<!-- custom -->
<script type="text/javascript" th:src="@{/js/WebSockets.js}"></script>
<script type="text/javascript" th:src="@{/js/game.js}"></script>
<link th:href="@{/css/game.css}" rel="stylesheet">
</div>
</body>
</html>
game.css
:
.card {
flex-direction: inherit !important;
}
.m-card {
display: inline-block;
position: relative;
background-color: #fff;
background-clip: border-box;
border: 1px solid rgba(0,0,0,.125);
border-radius: 0.25rem;
}
.other-hands {
max-height: 100px;
margin: 10px auto 0 auto;
}
.other-hands .m-card {
width: 60px;
height: 100px;
margin-left: -20px;
}
.other-hands-90 {
max-height: 325px;
padding-top: 17px;
margin: auto 0;
}
.other-hands-left {
margin-left: 10px;
}
.other-hands-right {
margin-right: 10px;
}
.other-hands-90 .m-card {
display: block;
width: 100px;
height: 60px;
margin-top: -35px;
}
.my-hands {
position: absolute;
max-height: 110px;
font-size: 35px;
word-break: normal;
left: 12px;
top: 490px;
}
.my-hands .m-card {
width: 80px;
height: 110px;
}
.card-type {
position: absolute;
top: 240px;
font-size: 35px;
word-break: normal;
}
.card-type .m-card {
width: 80px;
height: 110px;
}
.action {
position: absolute;
left: 487px;
top: 410px;
}
#button-pass {
margin-left: 50px;
}
.timer {
height: 50px;
width: 50px;
position: absolute;
padding: 1px;
border-radius: 100%;
border: 2px solid red;
font-size: 30px;
font-weight: bold;
color: red;
top: 30px;
left: 30px;
}
game.js
:
$(document).ready(() => {
relocateMyHands();
relocatePlayedCards();
})
function relocateMyHands() {
let newLocationX = ($(".game-window").width() - $(".my-hands").width()) / 2;
$(".my-hands").css("left", newLocationX);
}
function relocatePlayedCards() {
let newLocationX = ($(".game-window").width() - $(".card-type").width()) / 2;
$(".card-type").css("left", newLocationX);
}
package com.example.game;
import com.example.user.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
@Controller
public class GameController {
@Autowired
private UserService userService;
@GetMapping("/game/{roomId}")
public String viewGamePage(@PathVariable("roomId") String roomId, Model model) {
model.addAttribute("username", userService.getUsername());
return "game";
}
}
再來就直接去創建房間,然後開 4 隻帳號(對,你需要 4 個瀏覽器,或是 2 個分別都用一般跟無痕),然後大家一起準備好就開始吧~~